跳到主要内容

TCP 的连接管理

TCP 的连接建立

在连接建立中要解决如下三个问题

1、要使每一方知道对方的存在 2、要允许双方协商一些参数(如最大报文字段长度,最大窗口大小,服务质量等) 3、能够对运输实体资源进行分配和初始化(如缓存大小,各状态量,连接表中的项目等)

这个建立连接的过程也称为 “三次握手”

三次握手的过程

注意:TCP 协议是不分什么客户端服务端的,这里为了方便理解把对端称之为服务端

当客户端向服务端发起连接时,会先发一个连接请求数据过去询问服务端,能否建立连接,这包数据称之为 SYN

SYN=1ACK=0 时,表明这是一个连接请求报文段。服务端若同意建立连接,则服务端应在响应的报文段中使 SYN=1ACK=1,表示服务端同意连接。

客户端收到之后回复一个 ACK 包,只有当 ACK=1 时确认号字段才有效。当 ACK=0 时,确认号无效。

至此连接建立。

但是问题来了,为什么是三次握手而不是两次握手(即服务端回复 SYN + ACK 后就建立连接)?

这是为了防止已经失效的请求报文,突然又传到服务器引起错误

假设使用两次握手建立连接:

1、客户端发送一个 SYN 包请求连接,由于网络拥堵之类的原因,这个 SYN 包未到达服务端,滞留在中间某个节点了

2、为了建立连接(上一个包超时了),客户端又重新发送了一个 SYN 包,这次的包顺利抵达服务端,同时服务端回复 SYN + ACK 表示建立连接了

3、但是这时第一个由于数据阻塞的包突然恢复,第一个 SYN 包又送达了服务端。这时服务端会误认为是客户端又发起了一个新的连接

4、从而在两次握手之后,服务端进入等待数据状态(就是建立了新连接了)

5、这时服务端以为是两个连接,而客户端认为是一个连接,造成了状态不一致,所以需要使用三次握手的最后一个 ACK 包,服务端在收不到最后的 ACK 包的情况下自然不会认为连接建立成功

TCP 的连接释放

TCP 连接是全双工的,所以每个方向都必须要单独进行关闭(上行、下行)

四次挥手的过程

处于连接状态的客户端和服务端,都可以发起关闭连接请求,此时需要四次挥手来关闭连接

1、客户端发送一个 FTN=M,用来关闭客户端到服务器端的数据传送,客户端进入 等待1(FIN_WAIT_1) 状态,表示客户端已经没有数据要发送了,但是如果服务端还有数据没有发送完成则不用急着关闭连接,可以继续发送数据

2、服务端收到后发送一个 ACK=1 告诉客户端请求收到了,但是还有数据没有发完,请客户端继续等待,这时客户端进入 等待2(FIN_WAIT_2) 状态,继续等待服务端的 FIN 报文

3、当服务端确定数据已经发送完成,则向客户端发送 FIN 报文,告诉客户端,服务端已经做好准备关闭连接了,服务端进入 LAST_ACK 状态

4、客户端收到 FIN 报文后,再发一个 ACK 报文,告诉服务端自己已经收到关闭连接的通知了,但是这时客户端不是马上关闭,而是先进入 TIME_WAIT(超时等待)状态,因为担心服务端没有收到,客户端会先监听一段时间(2 * MSL 最长报文段寿命)

因为如果服务端没有收到客户端的 ACK 则会重新发一个 FIN 给客户端,客户端收到这个 FIN 会重新发送一个 ACK 给服务端。最后服务端收到 ACK 后就可以直接断开连接了,客户端等待了 2 * MSL(最长报文段寿命)都没有收到回复表示服务端已正常关闭了,这时客户端也可以关闭了

TCP 维持连接

参考资料 聊聊 TCP 长连接和心跳那些事

在学习 TCP 的连接维持之前,先来搞明白一个问题 TCP 是一个双向通信的协议,任一方都可以是发送者,那为什么那为什么还抽象了 Client 和 Server 呢? 因为 建立连接这件事就跟谈念爱一样,必须要有主动的一方,你主动我们就会有故事 。Client 可以理解为主动建立连接的一方,实际上两端的地位可以理解为是对等的。但是抽象后可以 方便理解 一些协议,例如 HTTP 协议的 request/response 模型,总之不要被这 Client 和 Server 两个词迷惑了

长连接与短连接

  • 短连接:每次通信时,创建 Socket;一次通信结束,调用 socket.close()。这就是一般意义上的短连接,短连接的好处是管理起来比较简单,存在的连接都是可用的连接,不需要额外的控制手段。
  • 长连接:每次通信完毕后,不会关闭连接,这样可以做到连接的复用。 长连接的好处是 省去了创建连接的耗时

短连接和长连接的优势,分别是对方的劣势。想要图简单,不追求高性能,使用短连接合适,这样我们就不需要操心连接状态的管理;想要追求性能,使用长连接,我们就需要担心各种问题:比如 端对端连接的维护,连接的保活。

连接的保活:KeepAlive

KeepAlive 并不是 TCP 协议的一部分,但是大多数操作系统都实现了这个机制(所以需要在操作系统层面设置 KeepAlive 的相关参数)。KeepAlive 机制开启后,在一定时间内(一般时间为 7200s,参数 tcp_keepalive_time)在链路上没有数据传送的情况下,TCP 层将发送相应的 KeepAlive 探针以确定连接可用性,探测失败后重试 10 次(参数 tcp_keepalive_probes),每次间隔时间 75s(参数 tcp_keepalive_intvl),所有探测失败后,才认为当前连接已经不可用

KeepAlive 机制是在 网络层面 保证了连接的可用性 ,但站在应用框架层面这还不够。主要体现在三个方面:

1、KeepAlive 的开关是在应用层开启的,但是具体参数(如重试测试,重试间隔时间)的设置却是操作系统级别的,位于操作系统的 /etc/sysctl.conf 配置中,这对于应用来说不够灵活。

2、KeepAlive 的保活机制只在链路空闲的情况下才会起到作用,假如此时有数据发送,且物理链路已经不通,操作系统这边的链路状态还是 ESTABLISHED,这时会发生什么?自然会走 TCP 重传机制,要知道默认的 TCP 超时重传,指数退避算法也是一个相当长的过程。

3、KeepAlive 本身是面向网络的,并不面向于应用,当连接不可用,可能是由于应用本身的 GC 频繁,系统 load 高等情况,但网络仍然是通的,此时,应用已经失去了活性,连接应该被认为是不可用的。

所以还需要在应用层也需要进行保活,因此引入了下面这个心跳检测机制

连接的保活:应用层心跳

就是客户端会开启一个 定时 任务,定时对已经建立连接的对端应用发送请求(这里的请求是特殊的心跳请求),服务端则需要特殊处理该请求,返回响应。如果心跳持续多次没有收到响应,客户端会认为连接不可用,主动断开连接。不同的服务治理框架对心跳,建连,断连,拉黑的机制有不同的策略,但大多数的服务治理框架都会在应用层做心跳

Reference

一条视频讲清楚TCP协议与UDP协议-什么是三次握手与四次挥手